package org.rcy.framework.data.config.mybatis.plugins;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.rcy.framework.data.transaction.RcyDataSourceContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

@Intercepts({
			@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) 
		})
public class SelectLimitPlugin implements Interceptor {

	private static Logger logger = LoggerFactory.getLogger(SelectLimitPlugin.class);
	
	private static Map<String,HashMap<String,Integer>> maxLimitMap = new HashMap<>();
	
	private RcyDataSourceContext dataSourceContext;


	public SelectLimitPlugin(RcyDataSourceContext dataSourceContext) {
		super();
		this.dataSourceContext = dataSourceContext;
	}

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		Object target = invocation.getTarget();
		if (target instanceof Executor) {
			return interceptExecutor(invocation);
		}
		return invocation.proceed();
	}

	private Object interceptExecutor(Invocation invocation) throws Throwable {
		if (invocation.getArgs().length != 4 || dataSourceContext.getMaxLimit() == 0 || dataSourceContext.getMaxLimit() == RowBounds.NO_ROW_LIMIT
				|| !(invocation.getTarget() instanceof Executor && invocation.getArgs().length == 4)) {
			return invocation.proceed();
		}

		int userMaxLimit = 0;
		String msId = null;
		String sqlString = null;
		try {
			final Object[] args = invocation.getArgs();
			MappedStatement ms = (MappedStatement) args[0];
			
			msId = ms.getId();
			logger.debug("SelectLimitPlugin interceptExecutor msId {}",msId);
			
			Object parameterObject = args[1];
			BoundSql boundSql = ms.getBoundSql(parameterObject);
			sqlString = boundSql.getSql();
			if (sqlString == null || "".equals(sqlString)) {
				return invocation.proceed();
			}
			sqlString = sqlString.replaceAll("[\\s]+", " ");
			if (!sqlString.toLowerCase().contains("limit")) {
				userMaxLimit = getUserMaxLimit(ms);
				RowBounds rowBounds = (RowBounds) invocation.getArgs()[2];
				if (rowBounds == null) {
					rowBounds = new RowBounds();
				}
				if (rowBounds.getLimit() == RowBounds.NO_ROW_LIMIT) {
					rowBounds = new RowBounds(rowBounds.getOffset(), userMaxLimit);
				}
				invocation.getArgs()[2] = rowBounds;
			}
		} catch (Exception e) {
			logger.error("SelectLimitPlugin interceptExecutor before error {}", e.getMessage(), e);
		}
		
		long t1 = System.currentTimeMillis();
		Object result = invocation.proceed();
		long cost = System.currentTimeMillis() - t1;
		
		try {
			int size = 0;
			if (result instanceof List) {
				List results = (List) result;
				size = results.size();
			}
			if (sqlString != null && !"".equals(sqlString) && dataSourceContext.getMaxLimit() != 0 && dataSourceContext.getMaxLimit() != RowBounds.NO_ROW_LIMIT  && size != 0 && userMaxLimit != 0 && size >= userMaxLimit || cost > dataSourceContext.getMaxLogTime()) {
				int index = indexOfSqlStart(sqlString);
				if (index > 0) {
					sqlString = sqlString.substring(index);
				}
				logger.warn("SelectLimitPlugin execute count {} cost {}ms method {} sql {}", size, cost, msId,sqlString);
			}
		} catch (Exception e) {
			logger.error("SelectLimitPlugin interceptExecutor after error {}", e.getMessage(), e);
		}
		
		return result;
	}
	
	private int getUserMaxLimit(MappedStatement ms) {
		String id = ms.getId();
		int lastIndexOf = id.lastIndexOf(".");
		String className = id.substring(0, lastIndexOf);
		String methodName = id.substring(lastIndexOf+1);
		if(maxLimitMap.containsKey(className)) {
			HashMap<String, Integer> methodMap = maxLimitMap.get(className);
			if(methodMap.containsKey(methodName)) {
				return methodMap.get(methodName)==null?dataSourceContext.getMaxLimit():methodMap.get(methodName);
			}else {
				return dataSourceContext.getMaxLimit();
			}
		}
		HashMap<String,Integer> methodMap = new HashMap<>();
		maxLimitMap.put(className,methodMap);
		
		try {
			Class clazz = Class.forName(className);
			Method[] methods = clazz.getMethods();
			for(Method method : methods) {
				SelectMaxLimit annotation = method.getAnnotation(SelectMaxLimit.class);
				if(annotation != null) {
					methodMap.put(method.getName(), annotation.value());
				}
			}
		} catch (Exception e) {
			logger.error("SelectLimitPlugin getUserMaxLimit error {}", e.getMessage(), e);
		} 
		return getUserMaxLimit(ms);
	}

	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
	}

	/**
	 * 获取sql语句开头部分
	 *
	 * @param sql
	 * @return
	 */
	private int indexOfSqlStart(String sql) {
		String upperCaseSql = sql.toUpperCase();
		Set<Integer> set = new HashSet<>();
		set.add(upperCaseSql.indexOf("SELECT "));
		set.add(upperCaseSql.indexOf("UPDATE "));
		set.add(upperCaseSql.indexOf("INSERT "));
		set.add(upperCaseSql.indexOf("DELETE "));
		set.remove(-1);
		if (set.size() == 0) {
			return -1;
		}
		List<Integer> list = new ArrayList<>(set);
		list.sort(Comparator.naturalOrder());
		return list.get(0);
	}
}
